/*******************************************************************************
 * Copyright (c) 2012, 2013 XML-Print Project.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     XML-Print Project - initial API and implementation
 ******************************************************************************/
package info.xmlprint.font;

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashSet;

public class FontFeature {
	private RandomAccessFile rf;

	// feature list
	private Feature[] featureList;

	// lookup list
	private Lookup[] lookupList;

	private Script[] scriptList;

	// lookup für gewuensche Feature
	private HashSet<LookupSubtable> subTablesToUse = new HashSet<LookupSubtable>();

	public static void main(String args[]) throws IOException {
		FontFeature gt = new FontFeature(
				"c:/windows/fonts/ACaslonPro-Regular.otf");
		System.out.println(gt.hasFeature("dflt", "DFLT", "smcp"));
	}

	public FontFeature(String filePath) {
		if (filePath == null)
			return;

		try {
			File file = new File(filePath);

			if (!file.exists()) {
				return;
			}

			RandomAccessFile raf = new RandomAccessFile(file, "r");
			rf = raf;
			// int version = raf.readInt();
			rf.skipBytes(4);

			short numTables = raf.readShort();

			/*
			 * short searchRange = raf.readShort(); short entrySelector =
			 * raf.readShort(); short rangeShift = raf.readShort();
			 */
			raf.skipBytes(6);

			long tag, gsubOffset = -1, offset;
			for (int i = 0; i < numTables; i++) {
				tag = raf.readInt();
				raf.readInt();
				offset = raf.readInt();
				raf.readInt();
				if (tag == 1196643650) {
					gsubOffset = offset;
				}
			}

			if (gsubOffset < 0)
				return;

			rf.seek(gsubOffset);
			// GSUB Header
			// int version = rf.readInt();
			rf.skipBytes(4);

			long scriptListOffset = rf.readUnsignedShort() + gsubOffset;
			long featureListOffset = rf.readUnsignedShort() + gsubOffset;
			long lookupListOffset = rf.readUnsignedShort() + gsubOffset;

			readScriptList(scriptListOffset);
			readFeatureList(featureListOffset);
			readLookupList(lookupListOffset);

			raf.close();

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// read scriptlist
	private void readScriptList(long scriptListOffset) throws IOException {

		rf.seek(scriptListOffset);
		int scriptCount = rf.readUnsignedShort();
		scriptList = new Script[scriptCount];
		// info:
		// System.out.println("script Count:" + scriptCount);

		String scriptTag;
		long scriptOffset;
		for (int i = 0; i < scriptCount; i++) {
			scriptTag = readStandardString(4);
			scriptOffset = rf.readUnsignedShort() + scriptListOffset;
			Script script = new Script();
			script.setName(scriptTag);
			script.setOffset(scriptOffset);
			scriptList[i] = script;
			// System.out.println("scriptTag " + script.getName() + " offset: "+
			// script.getOffset() + "");

		}
		// System.out.println("");
		for (Script scr : scriptList) {
			// display info:
			scr.readLangSys(rf);

		}
	}

	// read featurelist
	private void readFeatureList(long featureListOffset) throws IOException {

		rf.seek(featureListOffset);
		int featureCount = rf.readUnsignedShort();

		// display info:
		// System.out.println("\nfeature Count:" + featureCount);

		String featureTag;
		long featureOffset;
		featureList = new Feature[featureCount];
		for (int i = 0; i < featureCount; i++) {
			featureTag = readStandardString(4);
			featureOffset = rf.readUnsignedShort() + featureListOffset;
			Feature feature = new Feature();
			feature.setName(featureTag);
			feature.setOffset(featureOffset);
			featureList[i] = feature;
		}

		int j = 0;
		for (Feature f : featureList) {
			f.readLookups(rf);
			// info:
			// System.out.println(j + ": " + f.getName() + " offset:" +
			// f.getOffet() + " lookupCount:" + f.getLookupCount() + " "
			// + f.showLookupList());
			// j++;
		}

	}

	// read lookupList
	private void readLookupList(long lookupListOffset) throws IOException {

		rf.seek(lookupListOffset);
		int lookupCount = rf.readUnsignedShort();
		// System.out.println("\nLookup Count:" + lookupCount);
		int[] lookupOffsets = new int[lookupCount];
		for (int i = 0; i < lookupCount; i++) {
			lookupOffsets[i] = rf.readUnsignedShort();
		}
		lookupList = new Lookup[lookupCount];
		for (int i = 0; i < lookupCount; i++) {
			Lookup lookup = new Lookup(rf, lookupOffsets[i] + lookupListOffset);
			lookupList[i] = lookup;

			// display info:
			// System.out.println("lookup " + i + " offset:" +
			// lookup.getOffset()
			// + " subTableCount:" + lookup.getSubtableCount()
			// + lookup.showSubtables());
		}

	}

	/*
	 * gewuenschte feature festlegen, dann werden dazugehoerige LookupSubTable
	 * festgelegt. TODO: not used for checking smcp
	 */
	private void useFeature(String langTag, String scriptTag, String featureName) {
		// langTag = "DEU ";
		// scriptTag = "latn";

		Feature f;
		boolean b;
		for (int k = 0; k < featureList.length; k++) {

			f = featureList[k];

			if (featureName.equalsIgnoreCase(f.getName())) {
				// test mit langSys
				b = false;
				for (Script s : scriptList) {
					LangSysTable[] tables = s.getLangSystTableList();
					if (!s.getName().equalsIgnoreCase(scriptTag))
						continue;

					if (tables != null) {
						String tagName;
						LangSysTable defaultTable = null, table = null;

						for (LangSysTable t : tables) {
							tagName = t.getTag();
							if (tagName.equalsIgnoreCase(langTag)) {
								table = t;
								break;
							} else if (tagName.equalsIgnoreCase("DFLT")) {
								defaultTable = t;
							}
						}
						if (table == null) {
							table = defaultTable;
						}

						if (table != null && table.isFeatureIndexed(k)) {
							b = true;
							break;
						}
					}

					if (b)
						break;
				}

				if (!b) {
					continue;
				}

				int lookupIndex;
				Lookup lookup;
				for (int i = 0; i < f.getLookupCount(); i++) {
					lookupIndex = f.getLookupListIndex(i);
					lookup = lookupList[lookupIndex];
					for (int j = 0; j < lookup.getSubtableCount(); j++) {
						subTablesToUse.add(lookup.getSubtable(0));
						return;
					}
				}
			}
		}
	}

	public boolean hasFeature(String langTag, String scriptTag,
			String featureName) {
		// useFeature(langTag, scriptTag, featureName);
		/*
		 * if (subTablesToUse.size() > 0) { return true; }
		 */
		Feature f;
		boolean b;
		for (int k = 0; k < featureList.length; k++) {
			f = featureList[k];
			if (featureName.equalsIgnoreCase(f.getName())) {
				return true;
			}
		}
		return false;
	}

	/*
	 * new GlyphId zu ersetzen
	 */
	public int findNewGlyph(int glyphId) {
		int newGlyphId;
		for (LookupSubtable sub : subTablesToUse) {
			newGlyphId = sub.substitute(glyphId);
			if (newGlyphId != glyphId) {
				glyphId = newGlyphId;
				break;
			}
		}
		return glyphId;
	}

	protected String readStandardString(int length) throws IOException {
		byte buf[] = new byte[length];
		rf.readFully(buf);

		return new String(buf, "Cp1252");
	}

	/*
 * 
 */
	class Lookup {
		private int type;
		private int flag;
		private int subTableCount;
		private int[] subTableOffsets;
		private LookupSubtable[] subTables;
		private long offset;

		public Lookup(RandomAccessFile rf, long offset) throws IOException {
			this.offset = offset;
			rf.seek(offset);
			type = rf.readUnsignedShort();
			flag = rf.readUnsignedShort();
			subTableCount = rf.readUnsignedShort();
			subTableOffsets = new int[subTableCount];
			subTables = new LookupSubtable[subTableCount];
			for (int i = 0; i < subTableCount; i++) {
				subTableOffsets[i] = rf.readUnsignedShort();
			}

			for (int i = 0; i < subTableCount; i++) {
				subTables[i] = new LookupSubtable(type, rf, offset
						+ subTableOffsets[i]);
			}
		}

		public long getOffset() {
			return offset;
		}

		public int getType() {
			return type;
		}

		public String showSubtables() {
			String s = "";
			LookupSubtable subTable;
			for (int i = 0; i < subTableCount; i++) {
				subTable = subTables[i];
				s += " [first SubTable:" + " type:" + subTable.getType()
						+ " offset:" + subTable.getOffset() + "]";
			}
			return s;
		}

		public void findGlyph(int glyphId) {
			LookupSubtable subTable;
			for (int i = 0; i < subTableCount; i++) {
				subTable = subTables[i];
				// System.out.println(subTable.substitute(glyphId));
			}
		}

		public int getSubtableCount() {
			return subTableCount;
		}

		public LookupSubtable getSubtable(int i) {
			return subTables[i];
		}
	}

	/*
 * 
 */
	interface ILookupSubtable {
		public int substitute(int glyphId);
	}

	/*
 * 
 */
	class LookupSubtable implements ILookupSubtable {
		private int type;
		private long offset;
		private ILookupSubtable lookupSubtable;

		// 1 - Single - Replace one glyph with one glyph
		// 2 - Multiple - Replace one glyph with more than one glyph
		// 3 - Alternate - Replace one glyph with one of many glyphs
		// 4 - Ligature - Replace multiple glyphs with one glyph
		// 5 - Context - Replace one or more glyphs in context
		// 6 - Chaining - Context Replace one or more glyphs in chained context
		public LookupSubtable(int type, RandomAccessFile raf, long offset)
				throws IOException {
			this.offset = offset;
			this.type = type;

			switch (type) {
			case 1:
				raf.seek(offset);
				int format = raf.readUnsignedShort();
				if (format == 1) {
					lookupSubtable = new SingleSubstFormat1(raf, offset);
				} else if (format == 2) {
					lookupSubtable = new SingleSubstFormat2(raf, offset);
				}
				break;
			case 2:
				// todo
				break;
			case 3:
				raf.seek(offset);
				format = raf.readUnsignedShort();
				if (format == 1) {
					lookupSubtable = new SingleSubstFormat1(raf, offset);
				} else if (format == 2) {
					lookupSubtable = new SingleSubstFormat2(raf, offset);
				}
				break;
			case 4:
				// todo
				break;
			case 5:
				// todo
				break;
			case 6:
				// todo
				break;
			}

		}

		public int getType() {
			return type;
		}

		public long getOffset() {
			return offset;
		}

		@Override
		public int substitute(int glyphId) {
			if (lookupSubtable == null)
				return glyphId;

			return lookupSubtable.substitute(glyphId);

		}
	}

	/*
	 * 
	 */
	class SingleSubstFormat2 implements ILookupSubtable {
		private int coverageOffset;
		private int glyphCount;
		private int[] substitutes;
		private Coverage coverage;

		public SingleSubstFormat2(RandomAccessFile raf, long offset)
				throws IOException {
			coverageOffset = raf.readUnsignedShort();
			glyphCount = raf.readUnsignedShort();
			substitutes = new int[glyphCount];
			for (int i = 0; i < glyphCount; i++) {
				substitutes[i] = raf.readUnsignedShort();
			}
			raf.seek(offset + coverageOffset);
			coverage = new Coverage();
			coverage.read(raf);

		}

		public int getFormat() {
			return 2;
		}

		public int substitute(int glyphId) {
			int i = coverage.findGlyph(glyphId);
			if (i > -1) {
				return substitutes[i];
			}
			return glyphId;
		}

		public Coverage getCoverage() {
			return coverage;
		}
	}

	/*
	 * 
	 */
	class SingleSubstFormat1 implements ILookupSubtable {

		private int coverageOffset;
		private short deltaGlyphID;
		private Coverage coverage;

		public SingleSubstFormat1(RandomAccessFile raf, long offset)
				throws IOException {
			coverageOffset = raf.readUnsignedShort();
			deltaGlyphID = raf.readShort();
			raf.seek(offset + coverageOffset);
			coverage = new Coverage();
			coverage.read(raf);
		}

		public int getFormat() {
			return 1;
		}

		public int substitute(int glyphId) {

			int i = coverage.findGlyph(glyphId);
			if (i > -1) {
				return glyphId + deltaGlyphID;
			}
			return glyphId;
		}
	}

	/*
	 * 
	 */
	class Feature {
		private int featureParams;
		private int lookupCount;
		private int[] lookupListIndex;
		private long offset;
		private String name;

		public Feature() {
		}

		public long getOffet() {
			return offset;
		}

		public void setOffset(long featureOffset) {
			offset = featureOffset;
		}

		public void setName(String featureName) {
			name = featureName;
		}

		public String getName() {
			return name;
		}

		public void readLookups(RandomAccessFile raf) throws IOException {
			raf.seek(offset);
			featureParams = raf.readUnsignedShort();
			lookupCount = raf.readUnsignedShort();
			lookupListIndex = new int[lookupCount];
			for (int i = 0; i < lookupCount; i++) {
				lookupListIndex[i] = raf.readUnsignedShort();
			}
		}

		public int getLookupCount() {
			return lookupCount;
		}

		public int getLookupListIndex(int i) {
			return lookupListIndex[i];
		}

		public String showLookupList() {
			String s = "[";
			for (int i = 0; i < lookupCount; i++) {
				s += " " + lookupListIndex[i];
			}
			s += "]";
			return s;
		}

	}

	class Coverage {
		public int getFormat() {
			return 0;
		}

		public int findGlyph(int glyphId) {
			return 0;
		}

		protected Coverage read(RandomAccessFile raf) throws IOException {
			Coverage c = null;
			int format = raf.readUnsignedShort();
			if (format == 1) {
				c = new CoverageFormat1(raf);
			} else if (format == 2) {
				c = new CoverageFormat2(raf);
			}
			return c;
		}

		public int findGlyph(int glyph, int segCount) {
			return 0;
		}

	}

	class CoverageFormat1 extends Coverage {

		private int glyphCount;
		private int[] glyphIds;

		protected CoverageFormat1(RandomAccessFile raf) throws IOException {
			glyphCount = raf.readUnsignedShort();
			glyphIds = new int[glyphCount];
			for (int i = 0; i < glyphCount; i++) {
				glyphIds[i] = raf.readUnsignedShort();
			}
		}

		public int getFormat() {
			return 1;
		}

		public int findGlyph(int glyphId) {
			for (int i = 0; i < glyphCount; i++) {
				if (glyphIds[i] == glyphId) {
					return i;
				}
			}
			return -1;
		}

	}

	class CoverageFormat2 extends Coverage {

		private int rangeCount;
		private RangeRecord[] rangeRecords;

		protected CoverageFormat2(RandomAccessFile raf) throws IOException {
			rangeCount = raf.readUnsignedShort();
			rangeRecords = new RangeRecord[rangeCount];
			for (int i = 0; i < rangeCount; i++) {
				rangeRecords[i] = new RangeRecord(raf);
			}
		}

		public int getFormat() {
			return 2;
		}

		public int findGlyph(int glyphId) {
			for (int i = 0; i < rangeCount; i++) {
				int n = rangeRecords[i].getCoverageIndex(glyphId);
				if (n > -1) {
					return n;
				}
			}
			return -1;
		}

		public int findGlyph(int glyphId, int i) {
			if (i < 0 || i >= rangeCount)
				return -1;
			int n = rangeRecords[i].getCoverageIndex(glyphId);
			if (n > -1) {
				return n;
			}
			return -1;
		}

	}

	class RangeRecord {
		private int start;
		private int end;
		private int startCoverageIndex;

		public RangeRecord(RandomAccessFile raf) throws IOException {
			start = raf.readUnsignedShort();
			end = raf.readUnsignedShort();
			startCoverageIndex = raf.readUnsignedShort();
		}

		public boolean isInRange(int glyphId) {
			return (start <= glyphId && glyphId <= end);
		}

		public int getCoverageIndex(int glyphId) {
			if (isInRange(glyphId)) {
				return startCoverageIndex + glyphId - start;
			}
			return -1;
		}

	}

	class Script {

		private int langSysTableCount;
		private LangSysTable defaultLangSysTable;
		private LangSysTable[] langSysTableList;
		private String name;

		private long offset;

		protected void readLangSys(RandomAccessFile rf) throws IOException {
			rf.seek(offset);
			int defaultLangSysOffset = rf.readUnsignedShort();
			langSysTableCount = rf.readUnsignedShort();
			langSysTableList = null;
			if (langSysTableCount > 0) {
				langSysTableList = new LangSysTable[langSysTableCount];
				for (int i = 0; i < langSysTableCount; i++) {
					String langTag = readStandardString(rf, 4);
					long langOffset = rf.readUnsignedShort() + offset;
					LangSysTable table = new LangSysTable();
					table.setScript(this);
					table.setTag(langTag);
					table.setOffset(langOffset);
					langSysTableList[i] = table;
				}
			}

			// Read the LangSys tables
			if (langSysTableCount > 0) {
				for (int i = 0; i < langSysTableCount; i++) {
					langSysTableList[i].read(rf);
				}
			}
			if (defaultLangSysOffset > 0) {
				rf.seek(offset + defaultLangSysOffset);
				defaultLangSysTable = new LangSysTable();
				defaultLangSysTable.read(rf);
			}
		}

		public LangSysTable[] getLangSystTableList() {
			return langSysTableList;
		}

		public long getOffset() {
			return offset;
		}

		public void setOffset(long scriptOffset) {
			this.offset = scriptOffset;
		}

		public void setName(String name) {
			this.name = name;
		}

		public String getName() {
			return name;
		}

		public LangSysTable getDefaultLangSys() {
			return defaultLangSysTable;
		}

		private String readStandardString(RandomAccessFile rf, int length)
				throws IOException {
			byte buf[] = new byte[length];
			rf.readFully(buf);

			return new String(buf, "Cp1252");
		}
	}

	class LangSysTable {

		private int lookupOrder;
		private int reqFeatureIndex;
		private int featureCount;
		private int[] featureIndex;
		private long offset;
		private String tag;
		private Script script;

		protected void read(RandomAccessFile raf) throws IOException {
			raf.seek(offset);
			lookupOrder = raf.readUnsignedShort();
			reqFeatureIndex = raf.readUnsignedShort();
			featureCount = raf.readUnsignedShort();
			featureIndex = new int[featureCount];
			// System.out.println("\nlangTag:\"" + tag + "\"");
			for (int i = 0; i < featureCount; i++) {
				featureIndex[i] = raf.readUnsignedShort();

				/*
				 * String s = i + ""; if (script != null) { s += " Script:" +
				 * script.getName(); } s += "   featureIndex:" +
				 * featureIndex[i]; System.out.println(s);
				 */
			}
		}

		public void setScript(Script script) {
			this.script = script;
		}

		public long getOffset() {
			return offset;
		}

		protected boolean isFeatureIndexed(int n) {
			for (int i = 0; i < featureCount; i++) {
				if (featureIndex[i] == n) {
					return true;
				}
			}
			return false;
		}

		protected void setTag(String langTag) {
			this.tag = langTag;
		}

		public String getTag() {
			return tag;
		}

		protected void setOffset(long offset) {
			this.offset = offset;
		}

	}
}
